#!/usr/bin/env python3
# This software is distributed under GNU GPL license.
"""
Packet format:
 \x01 <command> \x1d <protover> <seqnum> <datalen> [ \x1d [ <varname> \x1f <varvalue> \x1e ]+ ] \x02 <data> \x04
  SOH            GS    uint16    uint32   uint64      GS               US              RS        STX         EOT
"""

# NOTES:
#   * With the current design, the entire "transfer unit" will be put into
#     memory, which is very undesirable. (TODO: work out a mechanism to tranfer
#     data by blocks (which is essentially exactly what TCP does.. Also,
#     this enables us to use lovely UDP.))


import socket, ssl
import sys, os, io
import shelve
import zlib
import configparser
import argparse
import logging
import multiprocessing, signal


class PacketCorrupted(Exception):
    pass

class ProtocolError(Exception):
    """
    Raised when it's been determined that
    peer talks to us in an unknown language.
    """
    pass


class Packet:
    """
    Describes a single transfer unit.
    """
    ...

class Protocol:
    """
    This class describes current protocol specs.

    Functions defined here are basically marcos for protocol manipulations.
    """
    # TODO: sanitize function names

    current_proto_ver = 1

    _SOH = b'\x01'
    _STX = b'\x02'
    _EOT = b'\x04'
    _GS = b'\x1d'
    _RS = b'\x1e'
    _US = b'\x1f'

    CLIENT_HELLO = 'sup'  # the 'command' field

    def parse_msg(msg, varname):
        # TODO: re-do this with the Packet class in mind. (sigh..)
        params = dict()

        rec_s = msg.find(_GS) + 1
        if msg[0] != _SOH or rec_s == 0:
            raise ProtocolError

        cmd = msg[1:rec_s - 1].decode()
        protover = int.from_bytes(msg[rec_s:rec_s + 2], 'big')
        seqnum   = int.from_bytes(msg[rec_s + 2:rec_s + 6], 'big')
        datalen  = int.from_bytes(msg[rec_s + 6:rec_s + 12], 'big')
        if protover != current_proto_ver:  # TODO
            raise ProtocolError

        rec_s += 2 + 4 + 8
        if msg[rec_s] == _STX:
            if len(msg[rec_s + 1:-1]) != datalen:
                raise PacketCorrupted
        elif msg[rec_s] == _GS:
            while not rec_s in (-1, 0) and msg[rec_s - 1] != _STX:
                rec_s += 1
                rec_d = msg.find(_US, rec_s)
                rec_e = msg.find(_RS, rec_d)
                if rec_e == -1 or rec_d == -1 or rec_e < rec_d:
                    raise ProtocolError

                name = msg[rec_s:rec_d].decode()
                value = msg[rec_d + 1:rec_e].decode()
                params[name] = value

                rec_s = rec_e

        if msg[-1] != _EOT:
            raise ProtocolError

        dataslice = slice(rec_s, -1)
        return (cmd, params, msg[dataslice])

    def check_client(sock, addr, client):
        """
        Checks that the peer speaks the same language as we do.
        Must only be called by Server.
        """
        try:
            packet = parse_msg(recv_msg(sock))
            if packet[0]
        except ProtocolError:
            return False
        except PacketCorrupted:
            # TODO: tell the client to try again
            send_msg(sock, comp_msg('tryagain',
                                    {'reason':'packetcorrupted'}, ''))
            return False
        return True

    def comp_msg(command, params, data):
        msg =  _SOH + command.encode() + _GS
        msg += current_proto_ver.to_bytes(2, 'big') + (1).to_bytes(4, 'big')
        msg += len(data).to_bytes(8, 'big') + ((_GS + params_b) if params_b)
        msg += _STX + data_b + _EOT
        return msg

    def send_msg(sock, msg):
        """
        As of now, this is simply a wrapper for socket.send()
        """
        sent = sock.send(msg)
        while sent < len(msg):
            sent += sock.send(msg[sent:])
        return None

    def recv_msg(sock):
        """
        As of now, this is simply a wrapper for socket.recv()
        """
        # FIXME: add data receiving loop OR leave it as it is and implement
        #        "blocky" data transferring
        return sock.recv(128 * 2 ** 20)  # 128 MiB


class SharedNode:
    """
    Class for shares with properties and stuff.
    """

    def __init__(self, path, mode='rl'):
        """
        path: relative path to the directory or file being shared
        mode: 'r' - read, 'w' - write, 'l' - allow to retrieve
        listing of files
        """
        # TODO: writable shares
        self.path = path
        self.mode = mode

    def walk(self, types='df.'):
        """
        include types: 'f' - files, 'd' - directories, '.' - hidden files
        """
        Ellipsis


class Client:
    pass


class ServerClient(multiprocessing.Process):
    """
    I don't know how to OOP, so there! Server client class.
    """
    # TODO: timeouts

    def __init__(self, socket, addr):
        multuprocessing.Process.__init__(self)
        self.socket = socket
        self.addr = addr
        self.proto_ver = None

    def stat(self):
        """
        Ask client for its status info.
        """
        ...

    def run(self):
        """
        Main method.
        Checks and accepts requests and sends data to the client.
        """
        while True:
            msg = Protocol.recv_msg(self.socket)
            request = Protocol.parsemsg(msg)

    def graceful_exit(self):
        """
        TERM signal handler
        """
        # TODO
        # TODO: SIGALRM ?
        ...


class Server:
    """
    Server class.
    """

    default_port = 3222
    default_proto_ver = Protocol.current_proto_ver

    def __init__(self, addr, shares,
                 proto_ver=default_proto_ver):
        """
        Create a server instance hosting shares and prepared to listen on
        host:port.
        """
        self.listen_addr = (addr[0] or socket.gethostname(),  # TODO: rethink this
                            addr[1] or default_port)
        self.listening = False
        self.shares = shares
        self.proto_ver = proto_ver
        self.clients = []  # list of all active clients

    def __repr__(self):
        return '<Server' + (' listening at {1}:{2}' if self.listening
                            else '') + \
               ' (proto_ver {3}) at 0x{0:X}>'.format(id(self),
               self.listen_addr[0], self.listen_addr[1] self.proto_ver)

    def start_server(self):
        """
        Start main server loop.
        """
        self.socket = socket.socket(type=socket.SOCK_STREAM)
        self.socket.bind(self.listen_addr)
        self.socket.listen(1)
        self.listening = True
        client_number = 0
        while True:
            (cli_socket, cli_addr) = self.socket.accept()
            # TODO: check that this client has good intentions, or else ban
            #   TODO: ban system
            client = ServerClient(cli_socket, cli_addr)
            if not Protocol.check_client(cli_socket, cli_addr, client):
                continue
            client_number += 1
            if client_number == 1337 or \
                    (client_number > 0 and
                            (client_number - 1 & client_number) == 0):
                # TODO
                print('Congratulations! You are client number {}! '
                      'You have won a prize!'.toupper().format(client_number))
            self.clients.append(client)
            client.start()  # fork off a child process serving this client
            # TODO: logging

    def stop_server(self):
        """
        Terminate the server.
        """
        self.socket.close()
        # TODO: close all communications with clients

    def kill_useless_children(self):
        """
        It's not as evil as it sounds.
        """
        kills = 0
        while True:
            (pid, status) = os.waitpid(0, os.WNOHANG)
            if (pid, status) == (0, 0):
                break
            #for i in range(len(self.clients)):
                # Alternatively I could shove the os.waitpid's in this cycle.
                # Current approach assumes that os.waitpid call takes longer
                # to execute than a cycle over the whole self.clients list.
                #if self.clients[i].pid == pid:
                    #del self.clients[i]
                    #break
            i = 0
            while i < len(self.clients) and self.clients[i].pid != pid:
                i += 1
            else:
                del self.clients[i]
                kills += 1
        return kills


class Netcp:
    """
    Top-level UI class.
    """

    @staticmethod
    def main():
        """public static void ..."""
        AP = argparse.ArgumentParser()
        AP.add_argument('cmd', choices=('server', 'add', 'remove', 'peers',
                                        'stop', 'get', 'status'))
        AP.add_argument('--node', '-n', action='append',
                        help='specify path to be included in shares list '
                             '(one per option)')
        AP.add_argument('--verbose', '-v', action='count')

        args = AP.parse_args()

        if args.cmd == 'server':
            signal.signal(signal.SIGCHLD, signal.SIG_IGN)  # TODO rethink this
            server = Server('', Server.port, args.node)
            server.start_server()  # TODO: terminate server
        else:
            print('not implemented')

        return None


if __name__ == "__main__":
    Netcp.main()
